﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using Boardgame;
using BoardgameModelViewed;


namespace BoardgameModel
{
    /// <summary>
    /// Represent and mediate a chess game.
    /// </summary>
    /// <remarks>
    /// This class is intended to be visible to the Controller in the MVC pattern.
    /// </remarks>
    public class ChessModel : ChessModelBase
    {
        public ChessModel ()
        {
            Setup (8, 8);
        }


        public ChessModel (int sizeX, int sizeY)
        {
            Setup (sizeX, sizeY);
        }


        /// <summary>
        /// Create a board with kings only.  More tokens may be added later.
        /// </summary>
        public ChessModel (int whiteKingX, int whiteKingY, int blackKingX, int blackKingY)
        {
            Clear (8, 8);

            if (whiteKingX < 0 || whiteKingX >= Board.SizeX)
                throw new ArgumentOutOfRangeException ("whiteKingX");

            if (whiteKingY < 0 || whiteKingY >= Board.SizeY)
                throw new ArgumentOutOfRangeException ("whiteKingY");

            if (blackKingX < 0 || blackKingX >= Board.SizeX)
                throw new ArgumentOutOfRangeException ("blackKingX");

            if (blackKingY < 0 || blackKingY >= Board.SizeY)
                throw new ArgumentOutOfRangeException ("blackKingY");

            if (whiteKingX == blackKingX && whiteKingY == blackKingY)
                throw new InvalidOperationException ("Cannot place kings on same square");

            Put (new King (Player.White), whiteKingX, whiteKingY);
            Put (new King (Player.Black), blackKingX, blackKingY);
        }


        public new bool IsThreefoldRepDrawOnMoveRequested
        {
            get { return isThreefoldRepDrawRequested; }
            set
            {
                if (! IsUnderway)
                    throw new InvalidOperationException ("Cannot draw a completed game.");

                if (value != isThreefoldRepDrawRequested)
                {
                    isThreefoldRepDrawRequested = value;
                    OnStatusUpdate();
                }
            }
        }

        #region Public instance methods

        public void Setup ()
        {
            Setup (Board.SizeX, Board.SizeY);
        }

        /// <summary>Create a board with standard setup.
        /// </summary>
        /// <remarks>
        /// If the supplied board width is less than 8, bishops will be removed
        /// followed by knights.  If the supplied board size is greater than 8,
        /// extra knights and pawns will be placed.
        /// </remarks>
        public void Setup (int sizeX, int sizeY)
        {
            if (sizeX < MinSize.X || sizeX > MaxSize.X)
                throw new ArgumentOutOfRangeException ("sizeX");

            if (sizeY < MinSize.Y || sizeY > MaxSize.Y)
                throw new ArgumentOutOfRangeException ("sizeY");

            Clear (sizeX, sizeY);

            Put (new King (Player.White), kingFile, 0);
            Put (new Queen (Player.White), kingFile-1, 0);
            Put (new Rook (Player.White), 0, 0);
            Put (new Rook (Player.White), Board.SizeX-1, 0);
            Put (new Bishop (Player.White), kingFile-2, 0);
            Put (new Bishop (Player.White), kingFile+1, 0);

            for (var knightFile = 1; knightFile < kingFile-2; ++knightFile)
                Put (new Knight (Player.White), knightFile, 0);

            for (var knightFile = kingFile+2; knightFile < Board.SizeX-1; ++knightFile)
                Put (new Knight (Player.White), knightFile, 0);

            for (var pawnFile = 0; pawnFile < Board.SizeX; ++pawnFile)
                Put (new Pawn (Player.White), pawnFile, 1);

            // Place black as a mirror of white.
            for (var pi = 0; pi < Pieces[Player.White].Count; ++pi)
            {
                var p1 = Pieces[Player.White][pi];
                var p2 = p1.MakeOpposite();
                Put (p2, p1.At.X, Board.SizeY - p1.At.Y - 1);
            }

            Status = GameStatus.Go;
            OnGameCreated (this);
            OnStatusUpdate();
        }


        /// <summary>Place a piece on the board.</summary>
        /// <param name="piece">Token to place.</param>
        /// <param name="cellX">Initial X coordinate of piece.</param>
        /// <param name="cellY">Initial Y coordinate of piece.</param>
        public void Put (Piece piece, int cellX, int cellY)
        {
            if (IsUnderway)
                throw new InvalidOperationException ("Game is underway");

            if (cellX < 0 || cellX >= Board.SizeX)
                throw new ArgumentOutOfRangeException ("cellX");

            if (cellY < 0 || cellY >= Board.SizeY)
                throw new ArgumentOutOfRangeException ("cellY");

            if (piece is King)
            {
                if (Pieces[piece.Side].Count > 0)
                    throw new ArgumentException ("Board already has a king for this player", "piece");
            }
            else if (Pieces[piece.Side].Count == 0)
                throw new ArgumentException ("King must be added first", "piece");

            if (Board[cellX, cellY].IsOccupied)
                throw new InvalidOperationException ("Square already occupied");

            if (piece.Game != null)
                throw new ArgumentException ("Piece already placed", "piece");

            piece.Game = (ChessModelBase) this;
            piece.At = new Cell (cellX, cellY);
            Board.squares[cellX, cellY] = piece;
            Pieces.Add (piece);
        }


        /// <summary>Assign promotion piece for next time pawn is promoted.
        /// </summary>
        /// <param name="pieceName">Name of promotion piece.</param>
        public void SetPromotion (string pieceName)
        {
            Player player = PlayerTurn;

            if (pieceName.Equals (typeof (Queen).Name, StringComparison.OrdinalIgnoreCase))
                SetPromotion (player, typeof (Queen));
            else if (pieceName.Equals (typeof (Rook).Name, StringComparison.OrdinalIgnoreCase))
                SetPromotion (player, typeof (Rook));
            else if (pieceName.Equals (typeof (Bishop).Name, StringComparison.OrdinalIgnoreCase))
                SetPromotion (player, typeof (Bishop));
            else if (pieceName.Equals (typeof (Knight).Name, StringComparison.OrdinalIgnoreCase))
                SetPromotion (player, typeof (Knight));
            else
                throw new ArgumentException ("Unknown promotion '" + pieceName + "'");
        }


        /// <summary>Assign promotion piece for next time pawn is promoted.
        /// </summary>
        /// <param name="player">Player of promotion assignment.</param>
        /// <param name="pieceType">Type of promotion piece.</param>
        public void SetPromotion (Player player, Type pieceType)
        {
            if (pieceType != typeof (Queen)
                && pieceType != typeof (Rook)
                && pieceType != typeof (Bishop)
                && pieceType != typeof (Knight))
                throw new ArgumentException ("Invalid promotion piece");

            Promotions[(int) player] = pieceType;
        }


        public void MakeMove (int fromX, int fromY, int toX, int toY)
        {
            if (Plies.Count == 0)
                if (startStatus == GameStatus.Setup)
                {
                    if (IsKingThreatened (Player.Black))
                        throw new InvalidOperationException ("Cannot begin with opposing king threat");

                    Status = GameStatus.Go;
                }

            if (!IsUnderway)
                throw new InvalidOperationException ("Cannot move piece when game is complete");

            if (fromX < 0 || fromY < 0 || toX >= Board.SizeX || toY >= Board.SizeY)
                throw new ArgumentException ("Coordinate out of bounds");

            Piece piece = Board[fromX, fromY] as Piece;
            if (piece == null || piece.Side != PlayerTurn)
                throw new InvalidOperationException ("Square not occupied by current player");

            if (! TryPushMove (fromX, fromY, toX, toY))
                throw new InvalidOperationException ("Illegal move");

            Status = CalcStatus();

            IsThreefoldRepRuleClaimable = CalcIsThreefoldRepRuleClaimable();
            IsFiftyMoveRuleClaimable = CalcIsFiftyMoveRuleClaimable();

            OnTokenRemove (fromX, fromY);

            Ply ply = Plies.Top;
            if (ply.IsPawnPromotion)
                OnTokenPlace (toX, toY, ply.Moving.Side, ply.Promotion.Token);
            else
            {
                OnTokenPlace (toX, toY, ply.Moving.Side, ply.Moving.Token);
                if (ply.IsCaptureEnPassant)
                    OnTokenRemove (toX, fromY);
                else if (ply.IsQueensideCastle)
                {
                    OnTokenRemove (0, fromY);
                    OnTokenPlace (toX + 1, toY, ply.Moving.Side, Token.Rook);
                }
                else if (ply.IsKingsideCastle)
                {
                    OnTokenRemove (Board.SizeY - 1, fromY);
                    OnTokenPlace (toX - 1, toY, ply.Moving.Side, Token.Rook);
                }
            }

            OnPieceMoved();
            OnStatusUpdate();

            if (IsThreefoldRepDrawOnMoveRequested)
            {
                if (IsThreefoldRepRuleClaimable)
                {
                    Status = GameStatus.Tie | GameStatus.Repetitive;
                    OnStatusUpdate();
                }
                else
                {
                    view.ShowMessage
                    (
                        "Your threefold repetition draw request is not valid and is denied!",
                        ChessModelBase.Opponent (PlayerTurn)
                    );

                    bool isAffirm = view.ShowQuestion
                    (
                        "Your opponent requested a draw that was not valid.  " +
                        "As a result, you have the option of declaring the game a draw.  " +
                        "Declare a draw?",
                        PlayerTurn
                    );

                    if (isAffirm)
                    {
                        Status = GameStatus.Tie;
                        OnStatusUpdate();
                    }
                    else
                        IsThreefoldRepDrawOnMoveRequested = false;
                }
            }
        }


        public void UndoMove ()
        {
            if (!IsUnderway || Plies.Count == 0)
                throw new InvalidOperationException ("Nothing to undo");

            Ply ply = Plies.Top;
            Player player = ply.Moving.Side;

            if (ply.Captured == null)
                OnTokenRemove (ply.To.X, ply.To.Y);
            else
            {
                int y;
                if (ply.IsCaptureEnPassant)
                {
                    y = ply.From.Y;
                    OnTokenRemove (ply.To.X, ply.To.Y);
                }
                else
                    y = ply.To.Y;

                OnTokenPlace (ply.To.X, y, ply.Captured.Side, ply.Captured.Token);
            }

            Token token;
            if (ply.Promotion != null)
                token = Token.Pawn;
            else
                token = ply.Moving.Token;

            OnTokenPlace (ply.From.X, ply.From.Y, player, token);

            if (ply.Moving is King)
            {
                if (ply.IsQueensideCastle)
                {
                    OnTokenPlace (0, ply.To.Y, player, Token.Rook);
                    OnTokenRemove (ply.To.X + 1, ply.To.Y);
                }
                else if (ply.IsKingsideCastle)
                {
                    OnTokenPlace (Board.SizeX - 1, ply.To.Y, player, Token.Rook);
                    OnTokenRemove (ply.To.X - 1, ply.To.Y);
                }
            }

            OnUndoPreview();
            Plies.Pop();
            IsThreefoldRepRuleClaimable = CalcIsThreefoldRepRuleClaimable();
            IsFiftyMoveRuleClaimable = CalcIsFiftyMoveRuleClaimable();
            OnStatusUpdate();
        }


        public void RequestFiftyMovesRuleDraw ()
        {
            if (! IsUnderway)
                throw new InvalidOperationException ("Cannot draw a completed game.");

            if (IsFiftyMoveRuleClaimable)
                Status = GameStatus.NoCaptures;
            else
            {
                view.ShowMessage
                (
                    "Your request for a draw on the fifty-moves rule is not valid!",
                    PlayerTurn
                );

                bool isAffirm = view.ShowQuestion
                (
                    "Your opponent requested a draw that was not valid.  " +
                    "As a result, you have the option of declaring the game a draw.  " +
                    "Declare a draw?",
                    ChessModelBase.Opponent (PlayerTurn)
                );

                if (! isAffirm)
                    return;

                Status = GameStatus.Tie;
            }

            OnStatusUpdate();
        }


        public void RequestThreefoldRepetitionDrawNow ()
        {
            if (! IsUnderway)
                throw new InvalidOperationException ("Cannot draw a completed game.");

            if (IsThreefoldRepRuleClaimable)
                Status = GameStatus.Repetitive;
            else
            {
                view.ShowMessage
                (
                    "Your request for a draw on the threefold repetition rule is not valid!",
                    PlayerTurn
                );

                bool isAffirm = view.ShowQuestion
                (
                    "Your opponent requested a draw that was not valid.  " +
                    "As a result, you have the option of declaring the game a draw.  " +
                    "Declare a draw?",
                    ChessModelBase.Opponent (PlayerTurn)
                );

                if (! isAffirm)
                    return;

                Status = GameStatus.Tie;
            }

            OnStatusUpdate();
        }


        public void RequestDraw ()
        {
            bool isAffirm = view.ShowQuestion
            (
                "Your opponent requested a draw.  " +
                "Do you agree to end the game in a tie?",
                ChessModelBase.Opponent (PlayerTurn)
            );

            if (isAffirm)
            {
                Status = GameStatus.Tie;
                OnStatusUpdate();
            }
            else
                view.ShowMessage
                (
                    "Your request for a draw has been denied by your opponent.",
                    PlayerTurn
                );
        }


        public void ResignGame ()
        {
            if (! IsUnderway)
                throw new InvalidOperationException ("Cannot resign a completed game.");

            Status = GameStatus.Resign;
            OnStatusUpdate();
        }


        public void BeginShowMoves (int cellX, int cellY)
        {
            OnHilitesClear();

            Piece piece = Board[cellX, cellY] as Piece;
            if (piece != null)
                foreach (Cell moveTo in piece.GetLegalMoves())
                    OnHiliteChange (moveTo.X, moveTo.Y, true);
        }


        public void EndShowMoves ()
        {
            OnHilitesClear();
        }

        #endregion

        #region Non-public instance methods

        private bool CalcIsThreefoldRepRuleClaimable ()
        {
            var reps = 3;

            if (Plies.Count < reps * 2)
                return false;

            var stateCount = new Dictionary<string, int> ();
            stateCount[startState] = 1;

            for (int p = Plies.Count-1; p >= 0; p = p-2)
            {
                int counter = 0;
                stateCount.TryGetValue (Plies[p].state, out counter);
                ++counter;
                stateCount[Plies[p].state] = counter;
                if (counter >= reps)
                    return true;
            }

            return false;
        }


        private void Clear (int sizeX, int sizeY)
        {
            if (Pieces == null)
                Pieces = new PieceLists();
            else
                Pieces.Clear();

            if (Plies == null)
                Plies = new PlyStack (this);
            else
                Plies.Clear();

            if (Board == null || Board.SizeX != sizeX || Board.SizeY != sizeY)
                Board = new Board (sizeX, sizeY);
            else
                Board.Clear();

            if (Promotions == null || Promotions.Length != 2)
                Promotions = new Type[2];

            Promotions[0] = typeof (Queen);
            Promotions[1] = typeof (Queen);

            kingFile = Board.SizeX / 2;
            isThreefoldRepDrawRequested = false;
            IsThreefoldRepRuleClaimable = false;
            IsFiftyMoveRuleClaimable = false;
            startState = null;
            Status = GameStatus.Setup;
        }


        // Returns true if legal move, false if illegal.
        private bool TryPushMove (int fromX, int fromY, int toX, int toY)
        {
            Piece piece = Board[fromX, fromY] as Piece;

            Debug.Assert (piece != null || piece.Side == PlayerTurn);

            // Try every legal move for this piece.
            foreach (Cell move in piece.GetLegalMoves())
                if (move.X == toX && move.Y == toY)
                {
                    Plies.Push (piece, toX, toY);
                    return true;
                }

            return false;
        }

        #endregion
    }
}
